{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is: JvID3v2Base.PAS, released on 2003-04-16.               *
*                                                                              *
* The Initial Developer of the Original Code is Remko Bonte. Portions created  *
* by Remko Bonte are Copyright (C) 2003 Remko Bonte. All Rights Reserved.      *
*                                                                              *
* Contributor(s): @home media limited (adapted for use with "mediate")         *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit MP3Info;

interface

uses Classes;

type
  TAHMMPEGLayer = (mlNotDefined, mlLayerIII, mlLayerII, mlLayerI);
  TAHMMPEGVersion = (mvVersion25, mvReserved, mvVersion2, mvVersion1);
  TAHMMPEGChannelMode = (mcStereo, mcJointStereo, mcDualChannel, mcSingleChannel);
  TAHMMPEGBit = (mbProtection, mbPrivate, mbCopyrighted, mbOriginal);
  TAHMMPEGBits = set of TAHMMPEGBit;
  TAHMMPEGEmphasis = (meNone, me5015ms, meReserved, meCCITJ17);
  TAHMMPEGModeExtension = (meModeExt0, meModeExt1, meModeExt2, meModeExt3);

  TAHMID3FileInfo = class(TPersistent)
  private
    FAudioSize: Int64;
    FBitrate: Integer;
    FBits: TAHMMPEGBits;
    FChannelMode: TAHMMPEGChannelMode;
    FEmphasis: TAHMMPEGEmphasis;
    FFileSize: Int64;
    FFrameCount: Integer;
    FFrameLengthInBytes: Integer;
    FHasID3v1Tag: Boolean;
    FHeaderFoundAt: Int64;
    FIsVBR: Boolean;
    FLayer: TAHMMPEGLayer;
    FLengthInSec: Integer;
    FModeExtension: TAHMMPEGModeExtension;
    FPaddingLength: Integer;
    FSamplingRateFrequency: Integer;
    FVersion: TAHMMPEGVersion;
    function GetIsValid: Boolean;
  protected
    procedure Calc;
    procedure ParseMPEGTag(AMPEGTag: PChar);
    procedure ParseVbrTag(AMPEGTag: PChar);
    procedure Reset;
  public
    procedure Read(Stream: TStream; const Offset: Int64);

    property Bitrate: Integer read FBitrate;
    property Bits: TAHMMPEGBits read FBits;
    property ChannelMode: TAHMMPEGChannelMode read FChannelMode;
    property Emphasis: TAHMMPEGEmphasis read FEmphasis;
    property FileSize: Int64 read FFileSize;
    property FrameCount: Integer read FFrameCount;
    property FrameLengthInBytes: Integer read FFrameLengthInBytes;
    property HeaderFoundAt: Int64 read FHeaderFoundAt;
    property IsValid: Boolean read GetIsValid;
    property IsVBR: Boolean read FIsVBR;
    property Layer: TAHMMPEGLayer read FLayer;
    property LengthInSec: Integer read FLengthInSec;
    property ModeExtension: TAHMMPEGModeExtension read FModeExtension;
    property SamplingRateFrequency: Integer read FSamplingRateFrequency;
    property Version: TAHMMPEGVersion read FVersion;
  end;

implementation

uses TagUtils;

const
  CMapBitrate: array [Boolean, TAHMMPEGLayer] of Byte =
   (
    //?? - III - II -  I
    ( $00, $02, $01, $00), // V1
    ( $00, $04, $04, $03) // V2/V3
   );

  CFreeBitrate = -2;

  CBadBitrate = -1;

  CBitrate: array [$00..$04, $00..$0F] of Integer =
   (
    (CFreeBitrate, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, CBadBitrate),
    (CFreeBitrate, 32, 48, 56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, 384, CBadBitrate),
    (CFreeBitrate, 32, 40, 48,  56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, CBadBitrate),
    (CFreeBitrate, 32, 48, 56,  64,  80,  96, 112, 128, 144, 160, 176, 192, 224, 256, CBadBitrate),
    (CFreeBitrate,  8, 16, 24,  32,  40,  48,  56,  64,  80,  96, 112, 128, 144, 160, CBadBitrate)
   );

  CSamplingFrequency: array [TAHMMPEGVersion, $00..$03] of Integer =
   (
    (11025, 12000,  8000, -1), // mvVersion25,
    (    0,     0,     0,  0), // mvReserved,
    (22050, 24000, 16000, -1), // mvVersion2,
    (44100, 48000, 32000, -1)  // mvVersion1
   );

  CLayerArray: array [TAHMMPEGLayer] of Integer =
   (
    1,           // mlNotDefined,
    144000,      // mlLayerIII,
    144000,      // mlLayerII,
    48000        // mlLayerI
   );


function SearchSync(Stream: TStream; const BeginOffset: Integer; var Buffer;
                    const BufferSize: Integer): Int64;
const
  CBufferSize = $0F00;
var
  LBuffer: PChar;
  I: Integer;
  LastWasFF: Boolean;
  BytesRead: Longint;
begin
  // Seek sync point 11111111 111
  LastWasFF := False;
  Result := Stream.Seek(BeginOffset, soFromBeginning);

  GetMem(LBuffer, CBufferSize);
  try
    while True do
    begin
      BytesRead := Stream.Read(LBuffer^, CBufferSize);
      if BytesRead = 0 then
      begin
        Result := -1;
        Break;
      end;

      for I := 0 to BytesRead - 1 do
      begin
        if LastWasFF and (Byte(LBuffer[I]) and $E0 = $E0) then
        begin
          Inc(Result, I - 1);
          if (I + BufferSize - 1 >= BytesRead) or (I = 0) then
          begin
            Stream.Seek(Result, soFromBeginning);
            if not Stream.Read(Buffer, BufferSize) = BufferSize then
              Result := -1;
          end
          else
            Move((LBuffer + I - 1)^, Buffer, BufferSize);

          Exit;
        end;

        LastWasFF := LBuffer[I] = #$FF;
      end;
      Inc(Result, BytesRead);
    end;
  finally
    FreeMem(LBuffer);
  end;
end;

procedure TAHMID3FileInfo.Calc;
const
  CID3v1Size: array [Boolean] of Integer = (0, 128);
var
  Tmp: Extended;
begin
  if FAudioSize = 0 then
    // No vbr tag found, so we calculate the audio size
    FAudioSize := FFileSize - FHeaderFoundAt - CID3v1Size[FHasID3v1Tag];

  if (FAudioSize > 0) and (FFrameCount > 0) then
  begin
    // We've found a vbr tag (with enough info)
    Tmp := FAudioSize / FFrameCount;
    FFrameLengthInBytes := Round(Tmp);

    // Determine average bitrate
    Tmp := FSamplingRateFrequency * Tmp / CLayerArray[Layer];
    if Version in [mvVersion2, mvVersion25] then
      Tmp := Tmp / 2;

    FBitrate := Round(Tmp);
    FLengthInSec := Trunc((FAudioSize * 8) / (1000 * Tmp));
  end
  else
  if FBitrate > 0 then
    FLengthInSec := Trunc((FAudioSize * 8) / (1000 * FBitrate));

  if FFrameLengthInBytes = 0 then
  begin
    // Didn't calc the FFrameLengthInBytes yet
    Tmp := 0;
    if (FBitrate <> CFreeBitrate) and (FSamplingRateFrequency > 0) then
    begin
      Tmp := CLayerArray[Layer] * FBitrate / FSamplingRateFrequency + FPaddingLength;
      if Version in [mvVersion2, mvVersion25] then
        Tmp := Tmp / 2;
    end;

    if Tmp > 0 then
    begin
      FFrameCount := Round(FAudioSize / Tmp);
      FFrameLengthInBytes := Round(Tmp);
    end;
  end;
end;

function TAHMID3FileInfo.GetIsValid: Boolean;
begin
  Result := (FHeaderFoundAt >= 0) and (FLayer <> mlNotDefined) and (FVersion <> mvReserved);
end;

procedure TAHMID3FileInfo.ParseMPEGTag(AMPEGTag: PChar);
var
  LHasPadding: Boolean;
  B: Byte;
begin
  // Parse protection bit
  B := PByte(AMPEGTag + 1)^;
  if B and $1 = 0 then Include(FBits, mbProtection);

  // Parse layer description
  B := B shr 1;
  FLayer := TAHMMPEGLayer(B and $3);
  B := B shr 2;
  FVersion := TAHMMPEGVersion(B and $3);
  if (FLayer = mlNotDefined) or (FVersion = mvReserved) then Exit;

  // Parse private bit
  B := PByte(AMPEGTag + 2)^;
  if B and $1 > 0 then Include(FBits, mbPrivate);

  // Parse padding bit
  B := B shr 1;
  LHasPadding := B and $1 > 0;

  // Parse sampling rate frequency
  B := B shr 1;
  FSamplingRateFrequency := CSamplingFrequency[Version, B and $3];

  // Parse bitrate
  B := B shr 2;
  FBitrate := CBitrate[CMapBitrate[Version in [mvVersion2, mvVersion25], Layer], B and $F];
  if FBitrate = CBadBitrate then Exit;

  // Parse emphasis
  B := PByte(AMPEGTag + 3)^;
  FEmphasis := TAHMMPEGEmphasis(B and $3);

  // Parse original/copy bit
  B := B shr 2;
  if B and $1 > 0 then Include(FBits, mbOriginal);

  // Parse copyright bit
  B := B shr 1;
  if B and $1 > 0 then Include(FBits, mbCopyrighted);

  // Parse mode extension
  B := B shr 1;
  FModeExtension := TAHMMPEGModeExtension(B and $3);

  // Parse channel mode
  B := B shr 2;
  FChannelMode := TAHMMPEGChannelMode(B and $3);

  // Calculate padding length
  if LHasPadding then
    if Layer = mlLayerI then
      FPaddingLength := 4
    else
      FPaddingLength := 1
  else
    FPaddingLength := 0;
end;

procedure TAHMID3FileInfo.ParseVbrTag(AMPEGTag: PChar);
const
  VBRTag_Xing: PChar = 'Xing';
  VBRTag_Info: PChar = 'Info';
  FRAMES_FLAG = $0001;
  BYTES_FLAG = $0002;
  TOC_FLAG = $0004;
var
  HeadFlags: Integer;
begin
  // Maximum bytes needed is currently: 4 + 32 + 4 + 4 + 4 + 4 = 52
  if Version = mvVersion1 then
  begin
    if ChannelMode <> mcSingleChannel then
      Inc(AMPEGTag, 32 + 4)
    else
      Inc(AMPEGTag, 17 + 4)
  end
  else
  begin
    if ChannelMode <> mcSingleChannel then
      Inc(AMPEGTag, 17 + 4)
    else
      Inc(AMPEGTag, 9 + 4);
  end;

  if (PInteger(AMPEGTag)^ <> PInteger(VBRTag_Xing)^) and
    (PInteger(AMPEGTag)^ <> PInteger(VBRTag_Info)^) then
    Exit;
  Inc(AMPEGTag, 4);

  FIsVBR := True;

  HeadFlags := Swap32(PInteger(AMPEGTag)^);
  Inc(AMPEGTag, 4);

  if HeadFlags and FRAMES_FLAG > 0 then
  begin
    FFrameCount := Swap32(PInteger(AMPEGTag)^);
    Inc(AMPEGTag, 4);
  end;

  if HeadFlags and BYTES_FLAG > 0 then
    FAudioSize := Swap32(PInteger(AMPEGTag)^);
end;

procedure TAHMID3FileInfo.Read(Stream: TStream; const Offset: Int64);
const
  CID3v1Tag = 'TAG';
  CTagSize = 128;
  CTagIDSize = 3;
  CMPEGTagSize = 52;
var
  TagID: array [0..CTagIDSize - 1] of Char;
  MPEGTag: array [0..CMPEGTagSize - 1] of Char;
begin
  Reset;

  FHeaderFoundAt := SearchSync(Stream, Offset, MPEGTag, CMPEGTagSize);
  if FHeaderFoundAt < 0 then Exit;

  ParseMPEGTag(MPEGTag);
  ParseVbrTag(MPEGTag);

  if FFileSize = 0 then FFileSize := Stream.Size;

  if (FAudioSize = 0) and (FFileSize >= 128) then
  begin
    // Need to determine if the file has an ID3v1 tag
    Stream.Seek(-CTagSize, soFromEnd);
    FHasID3v1Tag := (Stream.Read(TagID, CTagIDSize) = CTagIDSize) and (TagID = CID3v1Tag);
  end;

  // We now know enough to calculate the rest
  Calc;
end;

procedure TAHMID3FileInfo.Reset;
begin
  FAudioSize := 0;
  FBitrate := 0;
  FBits := [];
  FChannelMode := Low(TAHMMPEGChannelMode);
  FEmphasis := Low(TAHMMPEGEmphasis);
  FFileSize := 0;
  FFrameCount := 0;
  FFrameLengthInBytes := 0;
  FHasID3v1Tag := False;
  FHeaderFoundAt := -1;
  FIsVBR := False;
  FLayer := Low(TAHMMPEGLayer);
  FLengthInSec := 0;
  FModeExtension := Low(TAHMMPEGModeExtension);
  FSamplingRateFrequency := 0;
  FVersion := Low(TAHMMPEGVersion);
end;

end.
